跳到主要内容

Spring 事务属性详解~

转载自 JavaGuide Spring事务总结

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

举个例子!

我们在 A 类的 aMethod() 方法中调用了 B 类的 bMethod() 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。

如果我们的 bMethod() 如果发生异常需要回滚,如何配置事务传播行为才能让 aMethod() 也跟着回滚呢?

这个时候就需要事务传播行为的知识了,如果你不知道的话一定要好好看一下。

Class A {
@Transactional(propagation = propagation.xxx)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}

Class B {
@Transactional(propagation = propagation.xxx)
public void bMethod {
//do something
}
}

TransactionDefinition 定义中包括了如下几个表示传播行为的常量:

public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
......
}

为了方便使用,Spring 会相应地定义了一个枚举类:Propagation

public enum Propagation {
// 支持当前事务的情况:
// 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
// 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
// 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

// 不支持当前事务的情况:
// 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
// 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
// 以非事务方式运行,如果当前存在事务,则抛出异常。
NEVER(TransactionDefinition.PROPAGATION_NEVER),

// 其他情况:
// 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
// 如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
NESTED(TransactionDefinition.PROPAGATION_NESTED);


private final int value;

Propagation(int value) {
this.value = value;
}

public int value() {
return this.value;
}

}

正确的事务传播行为可能的值如下:

PROPAGATION_REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的 @Transactional 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

  1. 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2. 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。

举个例子:如果我们上面的 aMethod()bMethod() 使用的都是 PROPAGATION_REQUIRED 传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。

Class A {
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}

Class B {
@Transactional(propagation = Propagation.REQUIRED)
public void bMethod {
//do something
}
}

PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

举个例子:如果我们上面的 bMethod() 使用 PROPAGATION_REQUIRES_NEW 事务传播行为修饰,aMethod() 还是用 PROPAGATION_REQUIRED 修饰的话。如果 aMethod() 发生异常回滚,bMethod() 不会跟着回滚,因为 bMethod() 开启了独立的事务。但是,如果 bMethod() 抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod() 同样也会回滚,因为这个异常被 aMethod() 的事务管理机制检测到了。

Class A {
@Transactional(propagation = propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}

Class B {
@Transactional(propagation = propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}

PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。也就是说:

  • 在外部方法未开启事务的情况下 Propagation.NESTEDPropagation.REQUIRED 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • 如果外部方法开启事务的话,Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。

这里还是简单举个例子:

如果 aMethod() 回滚的话,bMethod()bMethod2() 都要回滚,而 bMethod() 回滚的话,并不会造成 aMethod()bMethod2() 回滚。

Class A {
@Transactional(propagation = propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
b.bMethod2();
}
}

Class B {
@Transactional(propagation = propagation.PROPAGATION_NESTED)
public void bMethod {
//do something
}
@Transactional(propagation = propagation.PROPAGATION_NESTED)
public void bMethod2 {
//do something
}
}

PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

这个使用的很少,就不举例子来说了。

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚,这里不对照案例讲解了,使用的很少。

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

事务隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

public interface TransactionDefinition {
......
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
......
}

它默认使用的隔离级别是根据 JDBC 对应的数据库决定的

和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:Isolation

public enum Isolation {
// 使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
// 最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
// 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
// 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
// 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行(串行),这样事务之间就完全不可能产生干扰,
// 也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

private final int value;

Isolation(int value) {
this.value = value;
}

public int value() {
return this.value;
}

}

因为平时使用 MySQL 数据库比较多,这里再多提一嘴!

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看,MySQL 8.0 该命令改为 SELECT @@transaction_isolation;

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+

要注意:MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是 Next-Key Locks(临键锁)

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容),但是 InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读) 并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

🌈拓展一下(以下内容摘自《MySQL技术内幕:InnoDB存储引擎(第2版)》7.7章):InnoDB 存储引擎提供了对 XA事务的支持,并通过 XA事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。

事务超时属性

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为 -1

事务只读属性

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();

}

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。

很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?

拿 MySQL 的 innodb 举例子,根据官网 autocommit, Commit, and Rollback 描述:MySQL 默认对每一个新建立的连接都启用了 autocommit 模式。在该模式下,每一个发送到 MySQL 服务器的 sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。

但是,如果你给方法加上了 Transactional 注解的话,这个方法执行的所有 sql 会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。

如果不加 Transactional,每条 sql 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。

  1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
  2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持

回滚规则

这些规则定义了哪些异常会导致事务回滚而哪些不会。

默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚。 但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。

同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

注意:RuntimeException 运行时异常和 Exception 非运行时异常。

事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

@Transactional 默认只捕获 RuntimeException,而自定义异常捕获后也不会回滚

注意:当 @Transactional 注解 作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常(RuntimeException),就会回滚,数据库里面的数据也会回滚。

但是如果在 @Transactional 注解中如果不配置 rollbackFor 属性(即默认的情况),那么事务只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor = Exception.class,可以让事务在遇到非运行时异常时也回滚。

使用例:

@Override
@Transactional(rollbackFor = TestException.class)
public void transOuter() {
productMapper.updateOrderQuantityPessimistic(product_code1);
((ProductService) AopContext.currentProxy()).transInner();
}

@Transactional(rollbackFor = Exception.class)
public void transInner() {
productMapper.updateOrderQuantityPessimistic(product_code);
if (true) {
throw new RuntimeException();
}
}

这样,自己抛出的 TestException 错误也能回滚~

总结:Spring 的事务传播级别

一、Spring 事务传播行为一共有7种类型,主要分为3类:

1)支持当前事物、 2)不支持当前事务、 3)奇葩类型。

支持当前事物

1)支持当前事物 —— PROPAGATION_REQUIRED:如果当前没有事物,就新建一个事务;如果有事物,就直接使用当前前事物、 2)支持当前事物 —— PROPAGATION_SUPPORTS :如果当前没有事务,就以非事务方式执行、 3)支持当前事物 —— PROPAGATION_MANDATORY:如果当前没有事务,就抛出异常。

不支持当前事物

1)不支持当前事物 —— PROPAGATION_REQUIRES_NEW:如果当前有事物,就将当前 前事物挂起,新建一个事物、 2)不支持当前事物 —— PROPAGATION_NOT_SUPPORTED:如果有事务,就将当前 前事物挂起,并以非事务方式执行、 3)不支持当前事物 —— PROPAGATION_NEVER:如果有事物,就抛异常,即必须以非事务方式执行。

奇葩类型

其实这是支持当前事物的特例 —— PROPAGATION_NESTED: 如果有事物,也新建一个事务,以事务嵌套事物的方式执行。

Reference

Spring事务传播行为7种类型 --- 看一遍就能记住!